![]() |
![]() |
|
Nutzen Sie eine generische Klasse wie Stack<T>, teilen Sie dem Compiler mit, durch welchen Datentyp T ersetzt werden soll. T wird auch als generischer Typparameter bezeichnet. In der folgenden Anweisung handelt es sich um int:
Alle Methoden der Klasse, die T als Parameter definieren oder zurückgeben, akzeptieren jetzt nur noch Integerwerte. Den Zugriff auf die generische Klasse Stack<T> zeigt nachfolgend die Methode Main. Der Code ist in einen try-Block gefasst, um ausgelöste Ausnahmen behandeln zu können. In Kapitel 9 werden wir das Thema Ausnahmen (Exceptions) noch ausgiebig behandeln.
Mehrere TypparameterDie zuvor verwendete generische Klasse Stack benötigte einen variablen Datentyp. Je nachdem, wie die zu entwickelnde Klasse aussehen soll, kann der Bedarf an Platzhaltern jedoch variieren. Ein typisches Beispiel dazu wäre eine Dictionary-Klasse, die nicht nur hinsichtlich des Schlüssels, sondern auch im Hinblick der zugeordnete Wert individuell typisiert werden soll. In solchen Fällen erlaubt C# die Angabe mehrerer Platzhalter, die innerhalb der spitzen Klammer mittels Kommata getrennt werden.
Der Zugriff auf eine Klasse mit mehreren Typparametern erfolgt genauso wie oben am Beispiel der Klasse Stack gezeigt. Jedem typisierten Parameter wird ein bekannter Typ in spitzen Klammern übergeben:
Das Schlüsselwort »default«Im Beispiel GenericDemo wird eine Exception geworfen, wenn die Methode Pop aufgerufen wird, der Stack aber bereits geleert ist. Eine andere Lösung hätte vermutlich auch zum Ziel geführt: eine Rückgabe mit return.
Dieser Ansatz ist richtig, solange der parametrisierte Typ T den Referenztypen zugerechnet werden kann. Handelt es sich jedoch um einen Wertetyp, wird die Laufzeit in einem Desaster enden, da null einem Wertetyp nicht zugewiesen werden kann; die Rückgabe müsste dann 0 sein. Die Lösung des Problems führt über das Schlüsselwort default. Dieses kann zwischen Referenz- und Wertetypen unterscheiden und liefert null, wenn es sich bei dem konkreten Typ um einen Referenztyp handelt, und 0, wenn es ein den Wertetypen zugerechneter Typ ist.
7.4.3 Typparameter mit Constraints einschränken
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public class GenericClass<T> { ... } |
teilen wir dem Compiler mit, dass der spätere Datentyp zum Zeitpunkt der Klassenentwicklung noch völlig unbekannt ist. Es kann jeder x-beliebige Datentyp verwendet werden.
Wollen Sie innerhalb des Codes der generischen Klasse ein bestimmtes Klassenmitglied des verwendeten Typs aufrufen (beispielsweise eine Methode), ist eine explizite und damit auch unsichere Konvertierung notwendig. Eventuelle Fehler, weil der verwendete Datentyp dieses Klassenmitglied nicht veröffentlicht, würden erst zur Laufzeit der Anwendung erkannt.
Um die Problematik zu verstehen, sehen Sie sich das folgende Beispiel an. Die Klasse SortedList<T> hat eine Methode Add, mit der ein Element der Auflistung hinzugefügt wird. Das neue Element soll nach typspezifischen Kriterien eingeordnet werden. Dazu ist ein Vergleich mit den schon enthaltenen Elementen notwendig, zu dem die Methode CompareTo der Schnittstelle IComparable genutzt wird.
| public class SortedList<T> { |
| T[] arr = new T[100]; |
| public T[] Add(T element) { |
| for(int i = 0; i < arr.Length; i++) { |
| int result = ((IComparable)element).CompareTo(arr[i]); |
| ... |
| } |
| ... |
| } |
| } |
Implementiert der Datentyp IComparable nicht, wird eine Ausnahme ausgelöst.
Um diesem Problem aus dem Weg zu gehen, lassen sich die Platzhalter mit Constraints versehen. Ähnlich einer SQL-Abfrage werden diese nach dem Schlüsselwort where notiert.
| public class SortableList<T> where T : IComparable { ... } |
Jetzt legen wir eine Bedingung fest, an die sich der spätere konkrete Typ halten muss: Er muss die aufgeführte Schnittstelle unterstützen.
In Add können wir nun sogar auf die explizite Konvertierung verzichten, denn wir schreiben die Implementierung der Schnittstelle dem Typ T vor:
| int result = element.CompareTo(arr[i]); |
Eine Bedingung ist nicht auf Schnittstellen beschränkt; Sie können auch eine Klasse angeben und legen damit die Basisklasse des an den Typparameter T übergebenen konkreten Typs fest.
Typparameter, die keinen Constraint aufweisen, werden als ungebundene Typparameter bezeichnet, mit Constraint als gebundene Typparameter. Im Bedarfsfall dürfen Sie auch mehrere Constraints angeben, die durch Kommata voneinander getrennt werden.
| public class SortableList<T> where T : IComparable, ICloneable { ... } |
Das Vorgehen mit dem Doppelpunkt erinnert an eine Klassenableitung. T muss in unserer Definition die angeführten Schnittstellen implementieren. Sie können auch die Basisklasse von T angeben und anschließend im Programmcode auf die Typkonvertierung verzichten und direkt auf die Mitglieder des parametrisierten Datentyps zugreifen.
Es lassen sich auch Bedingungen für mehrere Platzhalter festlegen. Dazu müssen Sie den Constraint für jeden einzelnen Platzhalter mit where einleiten:
| public class SpezializedList<T, K> |
| where T : IComparable, ICloneable |
| where K : SomeBaseClass |
| { ... } |
Nehmen wir an, Sie möchten in einer generischen Klasse ein Objekt vom Typ eines generischen Typparameters erzeugen. Das Problem dabei ist, dass der C#-Compiler nicht weiß, ob der Typparameter einen passenden Konstruktor hat. Die Folge wäre ein Kompilierfehler.
Um in dieser Situation eine Lösung zu bieten, können Sie der Liste der Constraints new() anhängen, wie im folgenden Codefragment gezeigt wird:
| public class MyList<K, V> where V : new() { |
| public K key = default(K); |
| public V value; |
| public MyList() { |
| value = new V(); |
| } |
| } |
In einer Constraint-Liste steht new() grundsätzlich immer am Ende, und Sie treffen damit eine entscheidende Aussage: Der gewählte Argumenttyp muss einen öffentlichen, parameterlosen Konstruktor unterstützen. Einen parametrisierten Konstruktor vorzuschreiben ist nicht möglich.
Generische Typen sind nicht nur im Zusammenhang mit Klassen möglich, sondern auch mit Methoden. Dabei ist es nicht zwingend notwendig, dass die Typparameter einer Methode denen der Klasse entsprechen:
| class GenericClass<T> { |
| public void GenericMethod<K>(K obj) { ... } |
| } |
Im Gültigkeitsbereich der Klasse ist in diesem Fall der Typ T bekannt, K nur innerhalb der Methode.
Sie dürfen generische, methodenspezifische Typparameter auch angeben, wenn die Klasse selbst keine definiert:
| class MyClass { |
| public void GenericMethod<K>(K obj) { ... } |
| } |
Eine Einschränkung sollten Sie sich aber merken:
| Eigenschaftsmethoden und Indexer unterstützen nur Typparameter, die sich im Gültigkeitsbereich der Klasse befinden. |
Der Aufruf einer Methode mit generischen Typparametern ist sehr einfach. Sie instanziieren in gewohnter Weise zuerst die Klasse und rufen die Methode unter Angabe des gewünschten konkreten Datentyps auf:
| GenericClass<string> obj = new GenericClass<string>(); |
| obj.GenericMethod<int>(25); |
Sie können sogar auf die Typangabe verzichten, denn auch in diesem Fall wird der C#-Compiler die richtige Schlussfolgerung ziehen:
| obj.GenericMethod(25); |
Dieser Aufruf ist absolut gleichwertig.
Muss der generische Typparameter einer Methode bestimmten Bedingungen genügen, legen Sie einen Constraint fest. Die Syntax entspricht der der Constraints einer Klasse. Allerdings ist es nicht möglich, einen Constraint für einen generischen Typparameter einer Methode zu definieren, der bereits auf Klassenebene festgelegt ist.
Dass Felder auf Klassenebene und Methodenparameter gleichnamig sein dürfen, ist Ihnen bekannt. Diese Freizügigkeit haben Sie mit generischen Typparametern nicht: Ein Platzhalter, der auf Klassenebene angegeben ist, darf für eine Methode nicht mehr verwendet werden, da der C#-Compiler nicht in der Lage ist, diese Doppeldeutigkeit aufzulösen.
| class GenericClass<T> { |
| // Fehlerhafter Typparameter |
| public T GenericMethod<T>(T obj) { ... } |
| } |
Generische Typparameter und Constraints können sowohl für Instanz- als auch für statische Methoden festgelegt werden, z.B.:
| class GenericClass<T> { |
| public static void GenericMethod<K>(T obj1, K obj2) { ... } |
| } |
Der Aufruf erfolgt mit
| GenericClass<string>.GenericMethod<int>("Hallo", 44); |
oder in verkürzter Form mit
| GenericClass<string>.GenericMethod("Hallo", 44); |
Generische Klassen können abgeleitet werden. Die Regeln sind ähnlich denen, die wir schon kennen. Aufgrund der besonderen Natur generischer Klassen sind dabei jedoch ein paar Besonderheiten zu beachten.
Ist die Basisklasse generisch, kann die abgeleitete Klasse den generischen Typparameter übernehmen und selbst generisch sein.
| class BaseClass<T> { ... } |
| class SubClass<T> : BaseClass<T> { ... } |
Die Basisklasse könnte die konkreten Datentypen durch einen Constraint auf ganz bestimmte Typen eingrenzen. Diese gilt dann auch für die abgeleitete Klasse und muss hinter der Basisklasse angegeben werden.
| class BaseClass<T> where T : IComparable { ... } |
| class SubClass<T> : BaseClass<T> where T : IComparable { ... } |
Soll die abgeleitete Klasse nicht generisch sein, muss der Typparameter in der Angabe der Basisklasse durch einen konkreten Datentyp ersetzt werden, wie nachfolgend gezeigt:
| class BaseClass<T> { ... } |
| class SubClass : BaseClass<int> { ... } |
Sie können umgekehrt auch dann eine generische Subklasse entwickeln, wenn die Basisklasse nicht generisch ist.
Sind in der Basisklasse virtuelle Methoden definiert, wird es noch einmal spannend, denn die Methode könnte in der Basisklasse einen generischen Typparameter haben. Virtuelle Methoden können mit override überschrieben werden. Ob der generische Typparameter durch einen konkreten Datentyp ersetzt werden muss oder ob der Typparameter auch in der überschreibenden Methode angeführt werden darf, entscheidet sich schon bei der Festlegung der Subklasse.
Spielen wir den Fall durch, dass die ableitende Klasse den geerbten generischen Typparameter konkret ersetzt, also:
| class BaseClass<T> { |
| public virtual T MyMethod() { ... } |
| } |
| class SubClass : BaseClass<int> { |
| public override int MyMethod() { ... } |
| } |
Wie weiter oben beschrieben, muss der Typparameter durch eine konkrete Angabe ersetzt werden. Das verpflichtet auch dazu, den gewünschten Datentyp in der Signatur der überschreibenden Methode zu benennen. Dass sich die Methode polymorph verhalten wird, bedarf kaum noch einer Erwähnung.
Soll auch die abgeleitete Klasse generisch sein, muss die virtuelle Methode mit generischen Typparametern überschrieben werden.
| class BaseClass<T> { |
| public virtual T MyMethod() { ... } |
| } |
| class SubClass<T> : BaseClass<T> { |
| public override T MyMethod() { ... } |
| } |
Eine implizite Konvertierung eines generischen Typparameters ist nur statthaft, wenn der Zieldatentyp object ist oder einer der Typen, die als Constraint hinter where angeführt sind.
| class ClassB<T> where T : ClassA, IComparable { |
| public void MyMethod(T obj) { |
| IComparable var1 = obj; |
| ClassA var2 = obj; |
| object var3 = obj; |
| } |
| } |
Die Klasse ClassB beschreibt den Typparameter T, der den folgenden Bedingungen genügen muss: Der konkrete Typ muss von der Klasse ClassA abgeleitet sein und das Interface IComparable implementieren. Die Zuweisungen in MyMethod sind damit gültig und typsicher, es wird kein Kompilierfehler erscheinen.
An ClassB wollen wir nun noch eine Manipulation vornehmen, indem wir auf die Constraints verzichten. Wir haben dann immer noch die Möglichkeit, implizit in object zu casten, die Konvertierung in eine Schnittstelle muss jedoch explizit erfolgen. Weil der Compiler zur Kompilierzeit nicht weiß, durch welchen konkreten Typ der Typparameter zur Laufzeit ersetzt wird, wird er diesen Cast akzeptieren. Nicht erlaubt ist hingegen die explizite Konvertierung in irgendeine Klasse.
| class B<T> { |
| public void MyMethod(T obj) { |
| IComparable var1 = (IComparable)obj; // korrekt !!! |
| ClassA var2 = (ClassA)obj; // fehlerhaft !!! |
| object var3 = obj; |
| } |
| } |
Es bleibt festzustellen, dass das explizite Casten nicht ganz ungefährlich ist und zur Laufzeit eine Ausnahme verursachen kann, wenn der generische Typ nicht die Schnittstelle implementiert. Um dieser Gefahrenquelle aus dem Weg zu gehen, bietet sich eine Alternative mit den beiden Operatoren is und as an. Zur Erinnerung: Mit beiden lässt sich der Typ einer Referenz überprüfen. is liefert true zurück, wenn der linke Operand vom Typ des rechten ist. Der as-Operator führt in diesem Fall sogar eine Konvertierung durch, andernfalls ist der Rückgabewert null.
Das folgende Codefragment zeigt, wie Sie die genannten Operatoren zur Typüberprüfung einsetzen können.
| class ClassB<T> { |
| public void MyMethod(T obj) { |
| if (obj is string) {...} |
| if (obj is IComparable) {...} |
| // alternativ: |
| int intVar = obj as string; |
| if (intVar != null) {...} |
| IComparable temp = obj as IComparable; |
| if (temp != null) {...} |
| } |
| } |
Delegate können außerhalb des Gültigkeitsbereichs einer Klasse oder in einer Klasse selbst definiert werden. Das gilt auch für Delegate, die einen generischen Typparameter beschreiben. Generische Delegate erweisen sich als besonders nützlich, wenn mehrere ähnliche Events ausgelöst werden. Ein kleiner Satz generischer Delegate, die sich in der Anzahl und dem Typ der Parameter unterscheiden, reicht oftmals vollkommen aus, um alle Ereignishandler bedienen zu können.
Sehen wir uns einen generischen Delegaten an, der außerhalb des Gültigkeitsbereichs einer Klasse definiert ist:
| public delegate void MyDelegate<T>(T obj); |
Auch hier gibt T den Parametertyp vor. Instanziiert wird ein generischer Delegat in der gleichen Weise wie jeder andere, also entweder
| MyDelegate<int> del = new MyDelegate<int>(MyEventHandler); |
oder einfach nur mit
| MyDelegate<int> del = MyEventHandler; |
Die Definition eines generischen Delegaten erlaubt uns außerdem, den konkreten Typ mit where zu beschränken. Wollen Sie beispielsweise den Typparameter T des Delegaten MyDelegate auf die Typen begrenzen, die von der Klasse ClassA abgeleitet sind und die Schnittstelle IMyInterface implementieren, würde die Anweisung wie folgt lauten:
| public delegate void MyDelegate<T>(T obj) where T : ClassA, IMyInterface; |
Die Generics haben erst in der Version 2.0 ihren Einzug in das .NET Framework gefunden und die Klassenbibliothek beeinflusst. Beispielsweise enthält .NET 2.0 mit System. Collections.Generic einen neuen Namespace, der eine Reihe generischer Auflistungen beinhaltet. Dieser Namespace enthält unter anderem auch eine Klasse Stack<T>, ähnlich der, die wir zu Anfang unserer Ausführungen zu den Generics beschrieben haben. Altbekannte Klassen des Frameworks 1.0/1.1 sind um generische Methoden ergänzt worden. Hier sei exemplarisch nur die Klasse Array erwähnt.
Viele andere Klassen und Schnittstellen des Namespace System.Collections finden ein generisches Pendant in System.Collections.Generic. In der folgenden Tabelle sind die wichtigsten Klassen und Schnittstellen des neuen Namespace aufgeführt nebst dem nichtgenerischen Implementierungen.
| System.Collections.Generic | System.Collections |
| Collection<T> | CollectionBase |
| Dictionary<K, V> | Hashtable |
| IComparer<T> | IComparer |
| IComparable<T> | IComparable |
| IEnumerable<T> | IEnumerable |
| IList<T> | IList |
| List<T> | ArrayList |
| Queue<T> | Queue |
| SortedDictionary<K, V> | SortedList |
| Stack<T> | Stack |
Nehmen wir an, wir hätten eine Klassendefinition wie folgt:
| public class Months { |
| string[] months = { "Januar", "Februar", "März", "April", |
| "Mai", "Juni", "Juli", "August", |
| "September", "Oktober", "November", "Dezember"}; |
| } |
Wäre es nicht schön, mit einer foreach-Schleife den Datenspeicher des Objekts months zu durchlaufen und Zugriff auf alle Elemente zu erhalten, etwa wie folgt:
| Months monate = new Months(); |
| foreach (string temp in monate) { |
| Console.WriteLine(temp); |
| } |
Dass daran Bedingungen geknüpft sind, habe ich in Abschnitt 7.3 eingangs schon erwähnt. Die Klasse Months muss dazu die Schnittstelle IEnumerable implementieren.
| public class Months : IEnumerable |
Die einzige in IEnumerable definierte Methode GetEnumerator liefert ein Objekt, das wiederum die Schnittstelle IEnumerator unterstützt.
| IEnumerator GetEnumerator (); |
Das IEnumerator-Objekt muss die Methoden MoveNext und Reset sowie die Eigenschaft Current implementieren. Damit wird das Durchlaufen der Klasse mit foreach möglich.
Die Beschreibung macht deutlich, dass einiges an Tippaufwand für die Codierung erforderlich ist. So war es jedenfalls im .NET Framework 1.0/1.1. Mit der Version 2.0 wird alles viel einfacher. Sie müssen zwar immer noch die Schnittstelle IEnumerable oder deren generisches Pendant und damit auch die Methode GetEnumerator implementieren, benötigen aber keinen IEnumerator-Typ mehr. Stattdessen liefern Sie die Daten nur noch mit dem neuen Schlüsselwort yield gefolgt von return aus.
| // -------------------------------------------------------------- |
| // Beispiel: ...\Kapitel 7\YieldDemo |
| // -------------------------------------------------------------- |
| class Program { |
| static void Main(string[] args) { |
| Months months = new Months(); |
| foreach(string temp in months) |
| Console.WriteLine(temp); |
| Console.ReadLine(); |
| } |
| } |
| public class Months : IEnumerable { |
| string[] month = { "Januar", "Februar", "März", "April", |
| "Mai", "Juni", "Juli", "August", "September", |
| "Oktober", "November", "Dezember"}; |
| // Methode der Schnittstelle 'IEnumerable' |
| public IEnumerator GetEnumerator() { |
| for (int i = 0; i < month.Length; i++) |
| yield return month[i]; |
| } |
| } |
yield in Kombination mit return wird zur Angabe des zurückgegebenen Wertes verwendet. Bei Erreichen von yield return wird die aktuelle Position gespeichert und beim nächsten Aufruf der Schleife die Ausführung von dieser Position neu gestartet. Mehr haben Sie als Entwickler nicht zu tun, denn im Hintergrund generiert der Compiler automatisch die Methoden Current und MoveNext der IEnumerator-Schnittstelle, wenn er yield erkennt.
Sie können das Programm sogar noch einfacher schreiben und auf die Implementierung von IEnumerable verzichten. Überlassen Sie einfach alles dem Compiler und yield return. Dazu schreiben Sie ebenfalls eine Methode, deren spezielle Aufgabe es ist, die Objektmenge zurückzuliefern. Allerdings dürfen Sie jetzt den Methodenbezeichner frei vergeben. Der Rückgabewert ist ein Objekt, das die Schnittstelle IEnumerable implementiert und somit auch implizit die Methode GetEnumerator. Hinter den Kulissen wird der Compiler dafür sorgen, dass der Iterator der anfragenden foreach-Schleife alle Daten der Reihe nach übergibt.
Das Beispiel YieldDemo_2 zeigt Ihnen, wie einfach jetzt der Code ist. Beachten Sie bitte auch, dass in der foreach-Schleife nun die Methode GetList für die Bereitstellung der Objekte sorgt. In diesem Code wird die generische Schnittstelle IEnumerable<T> angegeben, Sie können natürlich auch die untypisierte benutzen, was in diesem Fall gleichwertig ist.
| // ----------------------------------------------------------- |
| // Beispiel: ...\Kapitel 7\YieldDemo_2 |
| // ----------------------------------------------------------- |
| class Program { |
| static void Main(string[] args) { |
| Months months = new Months(); |
| foreach(string temp in months.GetList()) |
| Console.WriteLine(temp); |
| Console.ReadLine(); |
| } |
| } |
| public class Months { |
| string[] month = { "Januar", "Februar", "März", "April", |
| "Mai", "Juni", "Juli", "August", "September", |
| "Oktober", "November", "Dezember"}; |
| public IEnumerable<string> GetList() { |
| for (int i = 0; i < month.Length; i++) |
| yield return month[i]; |
| } |
| } |
Die Kombination yield return ist für den Compiler der Anstoß, automatisch einen Iterator zu erzeugen, der von einer foreach-Schleife genutzt werden kann.
Sie können auch mehrfach hintereinander yield aufrufen, wie das folgende Codefragment zeigt.
| // Methode der Schnittstelle 'IEnumerable' |
| public IEnumerator GetEnumerator() { |
| yield return "Aachen"; |
| yield return "Düsseldorf"; |
| yield return "Köln"; |
| } |
Zum Abbruch einer Iteration kombinieren Sie yield mit break:
yield break;
Der Einsatz von yield return unterliegt zwei Einschränkungen:
| yield return kann nicht innerhalb einer anonymen Methode codiert werden. |
| yield return darf weder in einem catch-Block noch in einem try-Block verwendet werden, wenn letzterer eine catch-Klausel hat. Die Verwendung in einem try-Block, dem sich nur noch ein finally-Block anschließt, ist jedoch möglich. |
Auch an dieser Stelle muss ich noch einmal darauf verweisen, dass Sie in Kapitel 9 alles über die Behandlung von Ausnahmen (Exceptions) mit try-catch erfahren werden.
Angenommen Sie greifen auf das Feld einer Tabelle in einer Datenbank zu. Der Datentyp des Feldes sei ein Integer. Damit ist der zulässige Wertebereich eines initialisierten Feldes bereits exakt beschrieben, der zwischen dem Minimal- und dem Maximalmalwert des Typs liegt. Spalten einer Datenbanktabelle müssen aber nicht zwangsläufig mit einem durch den Datentyp beschriebenen Wert gefüllt sein, sie dürfen auch leer bleiben und werden trotzdem als gültig anerkannt. In diesem Fall ist neben einem Zahlenwert auch null ein akzeptierter Inhalt.
Probleme dieser Art können nun ganz einfach durch Nullable Typen gelöst werden. Dabei spielt die im .NET Framework eingeführte Klasse System.Nullable<T> die entscheidende Rolle. Die Signatur deutet bereits an, dass die Klasse Generics benutzt. Dabei wird einem Datentyp die Verwendung von null ermöglicht. Das macht natürlich nur Sinn, wenn es sich bei dem Datentyp um einen Typ handelt, der den Wertetypen zugerechnet wird. Referenztypen unterstützen bekanntlich grundsätzlich null.
Grundsätzlich kann die Klasse wie folgt verwendet werden:
| Nullable<int> x = 4711; |
| Nullable<int> y = null; |
C# verfügt darüber hinaus auch über eine eigene Sprachsyntax. Dafür wurde der neue Modifizierer »?« eingeführt, der aus einem Datentyp einen null-fähigen Typen macht. Damit kann die Notation der beiden Anweisungen auch vereinfachend wie folgt lauten:
| int? x = 4711; |
| int? y = null; |
Da wir es jetzt mit einem neuen Datentypen zu tun haben, der auch null unterstützt, wird in der Klasse Nullable mit HasValues eine Eigenschaft angeboten, die einen booleschen Wert beschreibt. Er ist true, wenn der Inhalt der null-fähigen Variable einen gültigen Wert aufweist, also ungleich null ist.
| if (x.HasValue) |
| Console.WriteLine("Wert ist ungleich null"); |
| else |
| Console.WriteLine("Wert = null"); |
Die Ausgabe würde hier demnach lauten Wert ist ungleich null.
Der Inhalt der Variablen kann mit der Eigenschaft Value abgefragt werden. Sie liefert einen gültigen Wert, wenn HasValue true ist. Ansonsten wird eine Ausnahme vom Typ InvalidOperationException ausgelöst.
Darüber hinaus können Sie Nullable Typen auch in der üblichen Form eines Referenztypen verwenden und beispielsweise mit null vergleichen:
| if(x != null) { |
| ... |
| } |
Ein Nullable Typ ist gegenüber seinem zugrunde liegenden Datentyp um die Fähigkeit, auch null zu unterstützen, erweitert worden. Eine Zuweisung wie im folgenden Codefragment kommt einer aufweitenden Operation gleich und wird daher implizit vorgenommen.
| int x = 20; |
| int? y = x; |
Solch im umgehrten Fall die Zuweisung eines null-fähigen Typ an seinen elementaren Typ erfolgen, muss explizit konvertiert werden.
| int? x = 20; |
| int y = (int)x; |
Hat x in diesem Beispiel den Inhalt null, wird eine Ausnahme ausgelöst.
| << zurück |
|
||||||||||||||
|
||||||||||||||
|
||||||||||||||
|
||||||||||||||
Copyright © Galileo Press 2006
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.